本系列文以製作專案為主軸,紀錄小弟學習React以及GrahQL的過程。主要是記下重點步驟以及我覺得需要記憶的部分,有覺得不明確的地方還請留言多多指教。
要新增Todo,就是要在KanBan上的lists資料添上一筆,所以新增的方法一起建在KanBan裡是最方便的。
// KanBan.jsx
export default function KanBan() {
//...
const [lists, updateLists] = useState(dummyData);
//新增Todo方法
function addTodo(listIndex, newTodo) {
let newLists = [...lists];
newLists[listIndex].todos.push({ name: newTodo, finished: false });
updateLists(newLists);
}
// ...
}
在新增的時候必須知道現在是在哪個list上新增,所以要傳遞listIndex給NewTodo。
連帶新增Todo方法,把index傳給List再傳給NewTodo。
// KanBan.jsx
export default function KanBan() {
//...
return (
<span>
<KanBanNav />
<div fluid className="board p-1">
{lists.map((list, index) => (
// 傳 listId,addTodo 給 List
<List key={index} {...list} listId={index} addTodo={addTodo} />
))}
</div>
</span>
);
}
//List.jsx
export default function List({ title, todos, listId, addTodo }) {//解構賦值listId跟addTodo
//...
return(
//...
{showNew && (
<NewTodo
toggleShowNew={toggleShowNew}
//傳給NewTodo
listId={listId}
addTodo={addTodo}
/>
)}
//...
)
}
然後在NewTodo中運用傳進來的props新增Todo,不過因為有兩個事件onBlur,onInput都會執行新增、關閉輸入框的動作,所以先把整個流程包成一個方法。
export default function NewTodo({ listId, toggleShowNew, addTodo }) {//解構賦值
//...
function handleAddTodo(e) {
if (e.target.value.trim()) {//去掉頭尾空白,是空字串的話就不執行新增
addTodo(listId, e.target.value );
}
e.target.value = "";
toggleShowNew();
}
return (
<Form>
<Form.Control
as="textarea"
className="new-todo"
style={textareaStyle}
ref={newTodoRef}
onBlur={handleAddTodo} //新增事件
onInput={autoResize}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleAddTodo(e); //新增事件
}
}}
/>
</Form>
);
}
到這邊就可以在輸入後,按ENTER或點擊離開輸入框的時候新增Todo了。
上面的流程中,listId跟addTodo傳給List後沒做任何事就傳給了NewTodo,顯得有點冗。
回頭寫這段的時候就想有沒有辦法跳過List直接把props傳給NewTodo?於是才發現了composition這個方法。
React中有些props是被預先定義的,像是props.children,這個props代表了在JSX中被寫在children位置的物件。
像是:
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
在FancyBorder中寫了一個message,這個部分就會做為 props.children帶給FancyBorder,接著在FancyBorder就能存取:
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
<h1 className="Dialog-title">
Welcome
</h1>
{props.children} //message被加在這
</div>
);
}
歸根究柢就是把JSX作為props傳給component,所以也可以定義自己的props,帶入JSX:
function App() {
return (
<SplitPane
left={ //定義不同名稱的props
<Contacts /> //帶入想要的JSX
}
right={
<Chat />
} />
);
}
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left} //帶入的JSX顯示在這
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
用這個方式,把NewTodo在KanBan中就帶入,然後帶入listId跟addTodo:
//KanBan.jsx
return (
<span>
<KanBanNav />
<div fluid className="board p-1">
{lists.map((list, index) => (
<List
key={index}
{...list}
//新增一個NewTodo的 prop
NewTodo={<NewTodo listId={index} addTodo={addTodo} />}
/>
))}
</div>
</span>
);
到List中就能簡化成:
//List.jsx
export default function List({ title, todos, NewTodo }) {//解構NewTodo
//...
return (
<div className="list p-2 m-1 rounded-lg">
<div className="title">{title}</div>
{todos.map((todo) => (
<Todo key={todo.name} {...todo} />
))}
{showNew && NewTodo} //從props取用NewTodo
{!showNew && (
<div className="footer d-flex">...
)}
</div>
);
}
等等,原本從List傳給NewTodo的toggleShowNew方法要怎麼辦?
這要用另一個功能,React.cloneElement()。
這個功能可以複製元件後或添加/變更props,或變更children,在這裡用來增加新的props給NewTodo。
{showNew && React.cloneElement(NewTodo, { toggleShowNew })} //加入toggleShowNew
這樣就跟原本的功能一致了。